/**
 * 
 */
package com.db.stockexchange.server.orderbook;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map.Entry;
import java.util.TreeMap;
import java.util.logging.Logger;

import com.db.stockexchange.common.OrderStatus;


/**
 * @author Rayman
 * List of orders and operations with them
 */
public class OrderBook {
	static final float PRICE_TICK = 0.01f;

	private TreeMap<Price, OrderBookItem> book = new TreeMap<Price, OrderBookItem>();
	OrderBookItem marketOrders = new OrderBookItem(new Price(Float.NaN));
	
	OrderBookMatcher matcher;
	OrderBookMatcher marketMatcher;
	Price lastPrice = null;
	private String tickerName;
	Logger logger;
	
	public OrderBook(String tickerName, boolean usePartialFill) {
		this.tickerName = tickerName;
		if (usePartialFill) {
			matcher = new OrderPartialMatcher();
			marketMatcher = new MarketOrderPartialMatcher();				
		} else {
			matcher = new OrderEqualsMatcher();
			marketMatcher = new MarketOrderEqualsMatcher();			
		}
		logger = Logger.getLogger(this.getClass().getName());
		logger.fine(String.format("Order book by ticker <%s> created", this.tickerName));
	}
		
	public synchronized List<Deal> addOrder(Order order) throws IllegalOrderException {
		List<Deal> deals = matcher.match(order);
		
		if (order.getStatus() != OrderStatus.FILLED) {
			if (!order.isUseBestMarketPrice() || order.isUseBestMarketPrice() && lastPrice != null)
				// try to match with market orders
				deals.addAll(marketMatcher.match(order));
		}
		
		if (order.getStatus() != OrderStatus.FILLED) {
			if (order.isUseBestMarketPrice()) {
				marketOrders.add(order);
			}
			else
				findOrCreateOrderBookItemWithSpecifiedPrice(order.getPrice()).add(order);
			
			logger.fine(String.format("Order <%s> added to order book <%s>", order.toString(), this.tickerName));
		}
		else {
			if (!deals.isEmpty())
			lastPrice = deals.get(0).getPrice();
		}
		
		return deals;
	}
	
	private OrderBookItem findOrCreateOrderBookItemWithSpecifiedPrice(Price price) {
		OrderBookItem item = book.get(price);
		if (item == null) {
			item = new OrderBookItem(price);
			book.put(price, item);
		}
		
		return item;
	}
	
	private abstract class AbstractOrderMatcher implements OrderBookMatcher {

		public List<Deal> match(Order order) {
			if (book.isEmpty())
				return Collections.emptyList();
			
			Iterator<Entry<Price, OrderBookItem>> iterator = book.entrySet().iterator();
			
			switch (order.getSide()) {
			case BUY:
				if (order.isUseBestMarketPrice())
					iterator = book.entrySet().iterator();
				else
					iterator = book.headMap(order.getPrice(), true).entrySet().iterator();
				break;
			case SELL:
				if (order.isUseBestMarketPrice())
					iterator = book.descendingMap().entrySet().iterator();
				else
					iterator = book.tailMap(order.getPrice(), true).descendingMap().entrySet().iterator();
				break;
			}
			
			return matchWithRung(order, iterator);
		}

		private List<Deal> matchWithRung(Order order, Iterator<Entry<Price, OrderBookItem>> iterator) {
			List<Deal> deals = new ArrayList<>();
			while (iterator.hasNext()) {
				Entry<Price, OrderBookItem> item = iterator.next();
				
				List<Deal> res = matchWithQueue(order, item.getValue().getOrdersBySide(order.getSide()));
				deals.addAll(res);

				if (order.getStatus() == OrderStatus.FILLED)
					break;
			}
			
			return deals;
		}
		
		protected abstract List<Deal> matchWithQueue(Order order, List<Order> list);
	}
	
	public class OrderEqualsMatcher extends AbstractOrderMatcher {
				
		@Override
		protected List<Deal> matchWithQueue(Order order, List<Order> list) {
			Iterator<Order> iterator = list.iterator();
			while (iterator.hasNext()) {
				Order o = iterator.next();
				if (!o.getClientId().equals(order.getClientId()) && o.getOrderQuantity() == order.getOrderQuantity()) {
					iterator.remove();		
					order.fill(o.getOrderQuantity());
					o.fill(o.getOrderQuantity());
					return Arrays.asList(new Deal[] { new Deal(order, o, o.getPrice(), o.getOrderQuantity()) });
				}
			}
			
			return Collections.emptyList();
		}
	}

	
	public class OrderPartialMatcher extends AbstractOrderMatcher {
		
		@Override
		protected List<Deal> matchWithQueue(Order order, List<Order> list) {
			List<Deal> deals = new ArrayList<>();
			Iterator<Order> iterator = list.iterator();
			while (iterator.hasNext()) {
				Order o = iterator.next();
				if (!o.getClientId().equals(order.getClientId())) {
					int quantity = Math.min(order.getLeavesQuantity(), o.getLeavesQuantity());
					order.fill(quantity);
					o.fill(quantity);
					deals.add(new Deal(new Order(order), new Order(o), o.getPrice(), quantity));
					if (o.getStatus() == OrderStatus.FILLED)
						iterator.remove();
					
					if (order.getStatus() == OrderStatus.FILLED)
						break;
				}
			}
			
			return deals;
		}
	}
	
	private abstract class AbstractMarketOrderMatcher implements OrderBookMatcher {

		@Override
		public List<Deal> match(Order order) {
			switch (order.getSide()) {
			case BUY:
				if (marketOrders.getBids().isEmpty())
					return Collections.emptyList();
				else
					return matchWithQueue(order, marketOrders.getBids());
			case SELL:
				if (marketOrders.getAsks().isEmpty())
					return Collections.emptyList();
				else
					return matchWithQueue(order, marketOrders.getAsks());
			}

			return Collections.emptyList();
		}

		protected abstract List<Deal> matchWithQueue(Order order, List<Order> list);
	}
	
	public class MarketOrderEqualsMatcher extends AbstractMarketOrderMatcher {

		@Override
		protected List<Deal> matchWithQueue(Order order, List<Order> list) {
			Iterator<Order> iterator = list.iterator();
			while (iterator.hasNext()) {
				Order o = iterator.next();
				if (!o.getClientId().equals(order.getClientId()) && o.getOrderQuantity() == order.getOrderQuantity()) {
					iterator.remove();
					order.fill(o.getOrderQuantity());
					o.fill(o.getOrderQuantity());
					
					Price price = lastPrice;
					if (!order.isUseBestMarketPrice())
						price = order.getPrice();

					return Arrays.asList(new Deal[] { new Deal(order, o, price, o.getOrderQuantity()) });
				}
			}

			return Collections.emptyList();
		}
	}
	
	public class MarketOrderPartialMatcher extends AbstractMarketOrderMatcher {

		@Override
		protected List<Deal> matchWithQueue(Order order, List<Order> list) {
			List<Deal> deals = new ArrayList<>();
			Iterator<Order> iterator = list.iterator();
			while (iterator.hasNext()) {
				Order o = iterator.next();
				if (!o.getClientId().equals(order.getClientId())) {
					int quantity = Math.min(order.getLeavesQuantity(), o.getLeavesQuantity());
					order.fill(quantity);
					o.fill(quantity);

					Price price = lastPrice;
					if (!order.isUseBestMarketPrice())
						price = order.getPrice();
					
					deals.add(new Deal(new Order(order), new Order(o), price, quantity));
					if (o.getStatus() == OrderStatus.FILLED)
						iterator.remove();
					
					if (order.getStatus() == OrderStatus.FILLED)
						break;
				}
			}

			return deals;
		}
	}

	public void dispose() {
		for (Entry<Price, OrderBookItem> item : book.entrySet())
			item.getValue().dispose();
		book.clear();
	}
}
